# read in relevant libraries
library(data.table)
data.table 1.10.2
  The fastest way to learn (by data.table authors): https://www.datacamp.com/courses/data-analysis-the-data-table-way
  Documentation: ?data.table, example(data.table) and browseVignettes("data.table")
  Release notes, videos and slides: http://r-datatable.com
Warning message:
R graphics engine version 12 is not supported by this version of RStudio. The Plots tab will be disabled until a newer version of RStudio is installed. 
library(igraph)

Attaching package: ‘igraph’

The following objects are masked from ‘package:stats’:

    decompose, spectrum

The following object is masked from ‘package:base’:

    union
library(recommenderlab)
Loading required package: Matrix
Loading required package: arules

Attaching package: ‘arules’

The following objects are masked from ‘package:base’:

    abbreviate, write

Loading required package: proxy

Attaching package: ‘proxy’

The following object is masked from ‘package:Matrix’:

    as.matrix

The following objects are masked from ‘package:stats’:

    as.dist, dist

The following object is masked from ‘package:base’:

    as.matrix

Loading required package: registry

Attaching package: ‘recommenderlab’

The following objects are masked from ‘package:igraph’:

    normalize, similarity
library(ggplot2)
# set random seed
set.seed(23495)

The primary rating data was prepped in a separate code file, sampled, and and stored. The cleaned data is directly imported here for convenience.

# read in rating data 
rating.dt <- fread("netflix_sampled_data.csv", header=TRUE) # data for 2004, min. 100 user reviews and 100 movie ratings
# get data on movie names
## adjusted movie title names slightly directly in csv file prior to import
movies.dt <- fread("movie_titles_aws.csv", header=FALSE, col.names=c("MovieID", "Title"))

Basic summary statistics

# number of unique movies
cat("Number of unique movies:", length(unique(rating.dt$MovieID)))
Number of unique movies: 6177
# number of users
cat("\nNumber of users who provided ratings:",length(unique(rating.dt$CustomerID)))

Number of users who provided ratings: 51374
# number of total ratings
cat("\nNumber of total ratings:",nrow(rating.dt))

Number of total ratings: 8467727
# average and median user rating
cat("Average rating across data:",mean(rating.dt$Rating))
Average rating across data: 3.409843
cat("\nMedian rating across data:",median(rating.dt$Rating))

Median rating across data: 3
# distribution of average rating by user (indicating lack of uniformity)
# color options: #AA2B2B #9D2E2E ##98141D --> pptx dark red
avg.ratings <- rating.dt[, .(AvgRating=mean(Rating)), by=CustomerID]
ggplot(avg.ratings, aes(x=AvgRating)) + 
  geom_histogram(binwidth=0.2, col="gray", fill="#9D2E2E") + 
  labs(x="Average User Rating", y="Number of Users", title="Distribution of Average User Rating") + theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)), 
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("Average rating histogram.png", width=7, height=5)  

# distribution of movie rating by user
# color options: #AA2B2B #9D2E2E
ggplot(rating.dt, aes(x=Rating)) + 
  geom_histogram(binwidth=0.3, col="gray", fill="#9D2E2E") + 
  labs(x="User Rating of a Movie", y="Number of Users", title="Distribution of User Ratings") + 
  theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)), 
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("Rating Histogram.png", width=7, height=5)

# look at number of movies rated by user
# get table with number of movies rated by each user
user.ratings <- rating.dt[, .("NumRated"=.N), by=CustomerID]
# average number of movies rated
cat("Average number of movies rated:",mean(user.ratings$NumRated))
Average number of movies rated: 164.8251
# median number of movies rated
cat("\nMedian number of movies rated:",median(user.ratings$NumRated))

Median number of movies rated: 108
# max number of movvies rated
cat("\nMax number of movies rated:",max(user.ratings$NumRated))

Max number of movies rated: 5163

A number of users had very high movie ratings, e.g. in the 4000s that would imply 10+ movies seen per day on average. This may be due to multiple individuals sharing an account, or due to the use of on-site surveys to get ratings of movies a user saw in the past.

# get Top10 movies with highest number of ratings
movies.info <- rating.dt[, .("NumberofRatings"=.N, "AvgRating"=mean(Rating)), by=MovieID]
movies.info <- merge(movies.info, movies.dt, by="MovieID")
# get Top10 movies with highest number of ratings
print("Movies with highest number of ratings")
[1] "Movies with highest number of ratings"
head(movies.info[order(-NumberofRatings)]$Title, 10)
 [1] "My Big Fat Greek Wedding"                "Catch Me If You Can"                    
 [3] "Two Weeks Notice"                        "Sweet Home Alabama"                     
 [5] "Minority Report"                         "Road to Perdition"                      
 [7] "Signs"                                   "Harry Potter and the Chamber of Secrets"
 [9] "The Bourne Identity"                     "Lord of the Rings: The Two Towers"      
# get Top10 movies with highest average rating
print("Movies with highest average ratings")
[1] "Movies with highest average ratings"
head(movies.info[order(-AvgRating)]$Title, 10)
 [1] "Lord of the Rings: The Return of the King" "City of God"                              
 [3] "Alias: Season 2"                           "Raiders of the Lost Ark"                  
 [5] "CSI: Season 2"                             "24: Season 2"                             
 [7] "CSI: Season 1"                             "Family Guy: Vol. 2: Season 3"             
 [9] "The Sopranos: Season 2"                    "Alias: Season 1"                          
# Full plot
ggplot(movies.info, aes(x=NumberofRatings, y=AvgRating)) + 
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) + 
  labs(title="Average Rating versus Movie Degree Centrality", x="Movie Degree Centrality (in bipartite network)", y="Average Rating") + 
  theme_minimal() + scale_x_continuous(breaks=seq(0,60000,10000)) +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)), 
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("Average rating vs degree.png", width=7, height=5)

# Zoomed In Plot
ggplot(movies.info[NumberofRatings<3000,], aes(x=NumberofRatings, y=AvgRating)) + 
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) + 
  labs(title="Zoomed in: Average Rating versus Movie Degree Centrality", x="Movie Degree Centrality (in bipartite network)", y="Average Rating") + 
  theme_minimal() + scale_x_continuous(breaks=seq(0,3000,500)) +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)), 
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("Average rating vs degree_ZOOMED.png", width=7, height=5)

# Linear Regression on movies with 1500 or fewer ratings
regress.dt <- movies.info[NumberofRatings <= 1000,] # create subset of movies with 1500 or fewer ratings
setnames(regress.dt, "NumberofRatings", "Degree") # change name to degree
summary(lm(AvgRating~Degree, data=regress.dt)) # regression on subset

Call:
lm(formula = AvgRating ~ Degree, data = regress.dt)

Residuals:
     Min       1Q   Median       3Q      Max 
-1.68326 -0.30549  0.00245  0.31939  1.61046 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 3.112e+00  1.200e-02  259.40  < 2e-16 ***
Degree      1.785e-04  2.898e-05    6.16 7.91e-10 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4661 on 4409 degrees of freedom
Multiple R-squared:  0.008534,  Adjusted R-squared:  0.008309 
F-statistic: 37.95 on 1 and 4409 DF,  p-value: 7.908e-10
summary(lm(AvgRating~NumberofRatings, data=movies.info)) # regression on all data

Call:
lm(formula = AvgRating ~ NumberofRatings, data = movies.info)

Residuals:
     Min       1Q   Median       3Q      Max 
-1.70834 -0.29869  0.00949  0.32091  1.62039 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)     3.188e+00  6.524e-03  488.66   <2e-16 ***
NumberofRatings 3.462e-05  2.202e-06   15.72   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4546 on 6175 degrees of freedom
Multiple R-squared:  0.0385,    Adjusted R-squared:  0.03835 
F-statistic: 247.3 on 1 and 6175 DF,  p-value: < 2.2e-16
# distribution of number of movies rated by user, limit axis to 1000+ movies
plot.ratings <- user.ratings[,.(NumRated = ifelse(NumRated>=1000, 1000, NumRated))]
ggplot(plot.ratings, aes(x=NumRated)) + 
    geom_histogram(binwidth=8, col="gray", fill="#9D2E2E") + 
    labs(x="Number of Movies Rated", y="Number of Users", title="Distribution of Number of Movies Rated") + 
    theme_minimal() + scale_x_continuous(breaks=seq(100,1000,100),
                                     labels=c("100","200","300","400","500","600","700","800","900", "1000+")) +
  #scale_y_continuous(breaks=seq(0,60000,10000)) +
    theme(text=element_text(family="Roboto"),
          plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
          axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)), 
          axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("number of movies rated histogram.png", width=7, height=5)

Building Networks: MOVIE-USER NETWORK

# prep for making network
rating.dt[,CustomerID := sub("^", "u", CustomerID )]
rating.dt[, MovieID := as.character(MovieID)]
# make bipartite graph
graph.bp <- graph.data.frame(rating.dt[,1:2], directed=FALSE) # make general undirected graph
V(graph.bp)$type <- V(graph.bp)$name %in% rating.dt$MovieID # specify type to make bipartite
E(graph.bp)$weight <- rating.dt$Rating # add in rating as weight
# look at graph
graph.bp
IGRAPH UNWB 57551 8467727 -- 
+ attr: name (v/c), type (v/l), weight (e/n)
+ edges (vertex names):
 [1] u656399 --3 u1436762--3 u1644750--3 u616720 --3 u1614320--3 u115498 --3 u699878 --3 u2519847--3
 [9] u948069 --3 u67315  --3 u603277 --3 u1859725--3 u283774 --3 u1813349--3 u6689   --3 u109089 --3
[17] u525003 --3 u2312349--3 u1977959--3 u21983  --3 u2173816--3 u78931  --3 u2145227--3 u958104 --3
[25] u489962 --3 u206809 --3 u1007809--3 u1562675--3 u1477923--3 u44783  --3 u52540  --3 u870391 --3
[33] u2164676--3 u1281996--3 u2646060--3 u709342 --3 u1658752--3 u2266857--3 u1456369--3 u104768 --3
[41] u1355097--3 u1231910--3 u2599552--3 u153249 --3 u2590630--3 u203667 --3 u2338873--3 u719833 --3
[49] u2003554--3 u2213289--3 u2630072--3 u1614895--3 u1221390--3 u2193643--3 u357507 --3 u1599030--3
[57] u2443370--3 u871580 --3 u1733406--3 u309567 --3 u2096587--3 u290951 --3 u1213801--3 u1045221--3
+ ... omitted several edges
# Visualize graph layout
bp.subplot <- induced_subgraph(graph.bp,v=sample(unlist(V(graph.bp)$name), 7500))
# define color and shape mappings.
col <- c("gray85", "#9D2E2E")
shape <- c("circle", "square")
plot(bp.subplot,
  vertex.color = col[as.numeric(V(bp.subplot)$type)+1],
  vertex.shape = shape[as.numeric(V(bp.subplot)$type)+1], layout=layout_as_bipartite(bp.subplot, hgap=30),
  vertex.frame.color="gray60",
  edge.color = "#E5AAAA",
  vertex.label="", vertex.size=5)

Building Networks: MOVIE-MOVIE NETWORK

# make bipartite graph on movie-movie network
graph.bp2 <- graph.data.frame(rating.dt[,2:1], directed=FALSE) # make general undirected graph
V(graph.bp2)$type <- V(graph.bp2)$name %in% rating.dt$CustomerID # specify type to make bipartite
# look at graph
graph.bp2
IGRAPH UN-B 57551 8467727 -- 
+ attr: name (v/c), type (v/l)
+ edges (vertex names):
 [1] 3--u656399  3--u1436762 3--u1644750 3--u616720  3--u1614320 3--u115498  3--u699878  3--u2519847
 [9] 3--u948069  3--u67315   3--u603277  3--u1859725 3--u283774  3--u1813349 3--u6689    3--u109089 
[17] 3--u525003  3--u2312349 3--u1977959 3--u21983   3--u2173816 3--u78931   3--u2145227 3--u958104 
[25] 3--u489962  3--u206809  3--u1007809 3--u1562675 3--u1477923 3--u44783   3--u52540   3--u870391 
[33] 3--u2164676 3--u1281996 3--u2646060 3--u709342  3--u1658752 3--u2266857 3--u1456369 3--u104768 
[41] 3--u1355097 3--u1231910 3--u2599552 3--u153249  3--u2590630 3--u203667  3--u2338873 3--u719833 
[49] 3--u2003554 3--u2213289 3--u2630072 3--u1614895 3--u1221390 3--u2193643 3--u357507  3--u1599030
[57] 3--u2443370 3--u871580  3--u1733406 3--u309567  3--u2096587 3--u290951  3--u1213801 3--u1045221
+ ... omitted several edges
mov.mtx <- as_incidence_matrix(graph.bp2) # get affiliation matrix from chart
mov.sp.mtx <- as(mov.mtx, "sparseMatrix") # encode as sparse matrix
mov.coaffil.mtx <- tcrossprod(mov.sp.mtx) # get co-affiliation matrix to make movie network
# make movie-movie coaffiliation network
graph.movies <- graph_from_adjacency_matrix(mov.coaffil.mtx, mode="undirected", diag=FALSE, weight=TRUE) # keep diagonals because indicate own rating strength?

# calculate co-affiliation centrality measures
degree.score2 <- degree(graph.movies)
closeness.score2 <- closeness(graph.movies)
eigen.score2 <- eigen_centrality(graph.movies)
movies.ratings.2 <- rating.dt[MovieID %in% V(graph.movies)$name,
                               .("AvgRating"=mean(Rating)), by=MovieID]
movies.performance.2 <- data.table("MovieID"=V(graph.movies)$name, "Degree"=degree.score2,
                                   "Closeness"=closeness.score2)
movies.performance.2 <- data.table("MovieID"=V(graph.movies)$name, "Degree"=degree.score2, 
                                   "Closeness"=closeness.score2, "EigenCentrality"=eigen.score2$vector)
movies.performance.2 <- merge(movies.performance.2, movies.ratings.2, by="MovieID")
# Movie-Movie Degree Centrality
ggplot(movies.performance.2, aes(x=Degree, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus Movie-Movie Degree Centrality", x="Movie Degree Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("movie-movie degree vs avg rating.png", width=7, height=5)

# Movie-Movie Closeness Centrality
ggplot(movies.performance.2, aes(x=Closeness, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus Movie-Movie Closeness Centrality", x="Movie Closeness Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("movie-movie closeness vs avg rating.png", width=7, height=5)

# # Movie-Movie Eigen Centrality
ggplot(movies.performance.2, aes(x=EigenCentrality, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus Movie-Movie Eigen Centrality", x="Movie Eigen Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("movie-movie eigen vs avg rating.png", width=7, height=5)

# how to intepret
summary(lm(AvgRating ~ Degree, data = movies.performance.2)) # degree

Call:
lm(formula = AvgRating ~ Degree, data = movies.performance.2)

Residuals:
    Min      1Q  Median      3Q     Max 
-1.7551 -0.3049  0.0101  0.3276  1.5943 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  4.0413183  0.3881069  10.413   <2e-16 ***
Degree      -0.0001308  0.0000630  -2.076   0.0379 *  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4634 on 6175 degrees of freedom
Multiple R-squared:  0.0006977, Adjusted R-squared:  0.0005359 
F-statistic: 4.311 on 1 and 6175 DF,  p-value: 0.0379
summary(lm(AvgRating ~ Closeness, data = movies.performance.2)) # closeness

Call:
lm(formula = AvgRating ~ Closeness, data = movies.performance.2)

Residuals:
     Min       1Q   Median       3Q      Max 
-1.69906 -0.29266  0.00845  0.31557  1.65569 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  3.785e+00  2.851e-02  132.75   <2e-16 ***
Closeness   -9.606e+03  4.884e+02  -19.67   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4497 on 6175 degrees of freedom
Multiple R-squared:  0.05896,   Adjusted R-squared:  0.05881 
F-statistic: 386.9 on 1 and 6175 DF,  p-value: < 2.2e-16
summary(lm(AvgRating ~ EigenCentrality, data = movies.performance.2)) # EigenCentrality

Call:
lm(formula = AvgRating ~ EigenCentrality, data = movies.performance.2)

Residuals:
     Min       1Q   Median       3Q      Max 
-1.69644 -0.29628  0.00849  0.31753  1.62679 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)    
(Intercept)     3.176876   0.006693  474.67   <2e-16 ***
EigenCentrality 0.785185   0.045570   17.23   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4528 on 6175 degrees of freedom
Multiple R-squared:  0.04587,   Adjusted R-squared:  0.04572 
F-statistic: 296.9 on 1 and 6175 DF,  p-value: < 2.2e-16

Building Networks: USER-USER NETWORK

# limit user-user network to users with at least 200 ratings
users.sample <- copy(rating.dt)
users.sample <- users.sample[, NumRatings := .N, by=CustomerID]
users.sample <- users.sample[NumRatings >= 200, ]
# create bipartite graph with less data
graph.bp3 <- graph.data.frame(users.sample[,1:2], directed=FALSE) # make general undirected graph
V(graph.bp3)$type <- V(graph.bp3)$name %in% users.sample$MovieID # specify type to make bipartite
E(graph.bp3)$weight <- users.sample$Rating # add in rating as weight
# get coaffiliation matrix from bipartite graph
users.mtx <- as_incidence_matrix(graph.bp3) # get affiliation matrix from chart
users.sp.mtx <- as(users.mtx, "sparseMatrix") # encode as sparse matrix
users.coaffil.mtx <- tcrossprod(users.sp.mtx) # get co-affiliation matrix to make movie network
# make user-user coaffiliation network
graph.users <- graph_from_adjacency_matrix(users.coaffil.mtx, mode="undirected", diag=FALSE, weight=TRUE) # keep diagonals because indicate own rating strength?

# calculate co-affiliation centrality measures
degree.score3 <- degree(graph.users)
closeness.score3 <- closeness(graph.users)
eigen.score3 <- eigen_centrality(graph.users)
movies.ratings.3 <- rating.dt[CustomerID %in% V(graph.users)$name,
                               .("AvgRating"=mean(Rating)), by=CustomerID]
movies.performance.3 <- data.table("CustomerID"=V(graph.users)$name, "Degree"=degree.score3,
                                   "Closeness"=closeness.score3)
movies.performance.3 <- data.table("CustomerID"=V(graph.users)$name, "Degree"=degree.score3,
                                   "Closeness"=closeness.score3, "EigenCentrality"=eigen.score3$vector)
movies.performance.3 <- merge(movies.performance.3, movies.ratings.3, by="CustomerID")
# User-User Degree Centrality
ggplot(movies.performance.3, aes(x=Degree, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus User-User Degree Centrality", x="User Degree Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("user-user degree vs avg rating.png", width=7, height=5)

# User-User Closeness Centrality
ggplot(movies.performance.3, aes(x=Closeness, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus User-User Closeness Centrality", x="User Closeness Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("user-user closeness vs avg rating.png", width=7, height=5)

# # Movie-Movie Eigen Centrality
ggplot(movies.performance.3, aes(x=EigenCentrality, y=AvgRating)) +
  geom_smooth(method="loess", se=F, col="#9D2E2E", size=1.1) +
  labs(title="Average Rating versus User-User Eigen Centrality", x="User Eigen Centrality (in co-affiliation network)", y="Average Rating") +
  theme_minimal() +
  theme(text=element_text(family="Roboto"),
        plot.title=element_text(size=14, hjust=0.5, margin = margin(t = 5, r = 0, b = 8, l = 0)),
        axis.title.y = element_text(margin = margin(t = 0, r = 12, b = 0, l = 0)),
        axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)))
ggsave("user-user eigen vs avg rating.png", width=7, height=5)

# how to intepret
summary(lm(AvgRating ~ Degree, data = movies.performance.3)) # degree

Call:
lm(formula = AvgRating ~ Degree, data = movies.performance.3)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.41998 -0.26919  0.00282  0.27910  1.58002 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)
(Intercept) -248.09644  242.91656  -1.021    0.307
Degree         0.02119    0.02047   1.035    0.301

Residual standard error: 0.4334 on 11867 degrees of freedom
Multiple R-squared:  9.033e-05, Adjusted R-squared:  6.071e-06 
F-statistic: 1.072 on 1 and 11867 DF,  p-value: 0.3005
summary(lm(AvgRating ~ Closeness, data = movies.performance.3)) # closeness

Call:
lm(formula = AvgRating ~ Closeness, data = movies.performance.3)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.36042 -0.26918  0.00278  0.28199  1.64601 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 3.281e+00  1.664e-02 197.203   <2e-16 ***
Closeness   3.144e+04  3.650e+03   8.613   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4321 on 11867 degrees of freedom
Multiple R-squared:  0.006213,  Adjusted R-squared:  0.006129 
F-statistic: 74.19 on 1 and 11867 DF,  p-value: < 2.2e-16
summary(lm(AvgRating ~ EigenCentrality, data = movies.performance.3)) # EigenCentrality

Call:
lm(formula = AvgRating ~ EigenCentrality, data = movies.performance.3)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.41401 -0.26952  0.00205  0.27861  1.57799 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)      3.428648   0.009872 347.320   <2e-16 ***
EigenCentrality -0.035101   0.035585  -0.986    0.324    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.4334 on 11867 degrees of freedom
Multiple R-squared:  8.198e-05, Adjusted R-squared:  -2.278e-06 
F-statistic: 0.973 on 1 and 11867 DF,  p-value: 0.324

RECOMMENDER LAB

# get item-movie matrix from directed graph
input.mtx <- as_incidence_matrix(graph.bp, attr="weight", sparse=TRUE)
# store as recommender lab matrix #and normalize
input.mtx <- as(input.mtx, "realRatingMatrix")
# inspect the rating distributions
hist(getRatings(input.mtx))

hist(getRatings(normalize(input.mtx)), breaks=100)

hist(getRatings(normalize(input.mtx, method="Z-score")), breaks=100)

hist(colMeans(input.mtx), breaks=20)

Test example to produce sample recommendations (updated to give random sample)

# create a recommender on UBCF
rec.model = Recommender(input.mtx, method = "UBCF")
# get a random user name
test.user <- sample(rating.dt$CustomerID, 1)
# get the top 10 movies recommended for user XX
rowCounts(input.mtx[test.user])
u1887657 
    1062 
# what he rated high
rated.high <- rating.dt[CustomerID==test.user & Rating > 3,]
rec.test.user = predict(rec.model, input.mtx[test.user,], n=10)
rec.compare <- as.numeric(unlist(as(rec.test.user, "list")))
# movies he rated high
high <- movies.dt[MovieID %in% rated.high$MovieID, .(MovieID,Title)]
# movies recommended
recommend <- movies.dt[MovieID %in% rec.compare, .(MovieID,Title)]
high
recommend

Evaluation

# evaluate different methods
eval = evaluationScheme(input.mtx, method="split", train=0.75, given = 20, goodRating = 4)
eval
Evaluation scheme with 20 items given
Method: ‘split’ with 1 run(s).
Training set proportion: 0.750
Good ratings: >=4.000000
Data set: 51374 x 6177 rating matrix of class ‘realRatingMatrix’ with 8467727 ratings.
# algorithms (perform normalization automatically)
algorithms <- list(
  "random items" = list(name="RANDOM", param=NULL),
  "popular items" = list(name="POPULAR", param=NULL),
  "user-based CF" = list(name="UBCF", param=list(nn=50)),
  "item-based CF" = list(name="IBCF", param=list(k=50)),
  "SVD approximation" = list(name="SVD", param=list(k = 50)))
# evaluate top-N recommendations
results1 <- evaluate(eval, algorithms, type = "topNList", n=c(1, 5, 10, 20, 50))
RANDOM run fold/sample [model time/prediction time]
     1  [0.069sec/124.403sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [1.211sec/4112.694sec] 
UBCF run fold/sample [model time/prediction time]
     1  [1.228sec/36012.91sec] 
IBCF run fold/sample [model time/prediction time]
     1  [12666.36sec/16.449sec] 
SVD run fold/sample [model time/prediction time]
     1  [74.248sec/127.818sec] 
# ROC Curve
plot(results1, annotate=c(1,3), legend="bottomright", main="Comparison of ROC curves for 5 recommender methods")

# evaluate ratings prediction
results2 <- evaluate(eval, algorithms, type = "ratings")
RANDOM run fold/sample [model time/prediction time]
     1  [0.067sec/54.937sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [1.013sec/31.599sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0.914sec/34170.9sec] 
IBCF run fold/sample [model time/prediction time]
     1  [12895.81sec/12.755sec] 
SVD run fold/sample [model time/prediction time]
     1  [73.395sec/56.64sec] 
# MSE / MAE plot for rating
plot(results2, ylim = c(0,3), main="Comparison of RMSE, MSE, and MAE for 5 recommender methods")

LS0tCnRpdGxlOiAiU05fUHJvamVjdF9WMSIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3J9CiMgcmVhZCBpbiByZWxldmFudCBsaWJyYXJpZXMKbGlicmFyeShkYXRhLnRhYmxlKQpsaWJyYXJ5KGlncmFwaCkKbGlicmFyeShyZWNvbW1lbmRlcmxhYikKbGlicmFyeShnZ3Bsb3QyKQpgYGAKCmBgYHtyfQojIHNldCByYW5kb20gc2VlZApzZXQuc2VlZCgyMzQ5NSkKYGBgCgpUaGUgcHJpbWFyeSByYXRpbmcgZGF0YSB3YXMgcHJlcHBlZCBpbiBhIHNlcGFyYXRlIGNvZGUgZmlsZSwgc2FtcGxlZCwgYW5kIGFuZCBzdG9yZWQuIFRoZSBjbGVhbmVkIGRhdGEgaXMgZGlyZWN0bHkgaW1wb3J0ZWQgaGVyZSBmb3IgY29udmVuaWVuY2UuCmBgYHtyfQojIHJlYWQgaW4gcmF0aW5nIGRhdGEgCnJhdGluZy5kdCA8LSBmcmVhZCgibmV0ZmxpeF9zYW1wbGVkX2RhdGEuY3N2IiwgaGVhZGVyPVRSVUUpICMgZGF0YSBmb3IgMjAwNCwgbWluLiAxMDAgdXNlciByZXZpZXdzIGFuZCAxMDAgbW92aWUgcmF0aW5ncwoKIyBnZXQgZGF0YSBvbiBtb3ZpZSBuYW1lcwojIyBhZGp1c3RlZCBtb3ZpZSB0aXRsZSBuYW1lcyBzbGlnaHRseSBkaXJlY3RseSBpbiBjc3YgZmlsZSBwcmlvciB0byBpbXBvcnQKbW92aWVzLmR0IDwtIGZyZWFkKCJtb3ZpZV90aXRsZXNfYXdzLmNzdiIsIGhlYWRlcj1GQUxTRSwgY29sLm5hbWVzPWMoIk1vdmllSUQiLCAiVGl0bGUiKSkKYGBgCgoqKioqKioqKioqKioqKioKCiMjIyMgQmFzaWMgc3VtbWFyeSBzdGF0aXN0aWNzCgpgYGB7cn0KIyBudW1iZXIgb2YgdW5pcXVlIG1vdmllcwpjYXQoIk51bWJlciBvZiB1bmlxdWUgbW92aWVzOiIsIGxlbmd0aCh1bmlxdWUocmF0aW5nLmR0JE1vdmllSUQpKSkKCiMgbnVtYmVyIG9mIHVzZXJzCmNhdCgiXG5OdW1iZXIgb2YgdXNlcnMgd2hvIHByb3ZpZGVkIHJhdGluZ3M6IixsZW5ndGgodW5pcXVlKHJhdGluZy5kdCRDdXN0b21lcklEKSkpCgojIG51bWJlciBvZiB0b3RhbCByYXRpbmdzCmNhdCgiXG5OdW1iZXIgb2YgdG90YWwgcmF0aW5nczoiLG5yb3cocmF0aW5nLmR0KSkKYGBgCgpgYGB7cn0KIyBhdmVyYWdlIGFuZCBtZWRpYW4gdXNlciByYXRpbmcKY2F0KCJBdmVyYWdlIHJhdGluZyBhY3Jvc3MgZGF0YToiLG1lYW4ocmF0aW5nLmR0JFJhdGluZykpCmNhdCgiXG5NZWRpYW4gcmF0aW5nIGFjcm9zcyBkYXRhOiIsbWVkaWFuKHJhdGluZy5kdCRSYXRpbmcpKQpgYGAKCmBgYHtyfQojIGRpc3RyaWJ1dGlvbiBvZiBhdmVyYWdlIHJhdGluZyBieSB1c2VyIChpbmRpY2F0aW5nIGxhY2sgb2YgdW5pZm9ybWl0eSkKIyBjb2xvciBvcHRpb25zOiAjQUEyQjJCICM5RDJFMkUgIyM5ODE0MUQgLS0+IHBwdHggZGFyayByZWQKYXZnLnJhdGluZ3MgPC0gcmF0aW5nLmR0WywgLihBdmdSYXRpbmc9bWVhbihSYXRpbmcpKSwgYnk9Q3VzdG9tZXJJRF0KZ2dwbG90KGF2Zy5yYXRpbmdzLCBhZXMoeD1BdmdSYXRpbmcpKSArIAogIGdlb21faGlzdG9ncmFtKGJpbndpZHRoPTAuMiwgY29sPSJncmF5IiwgZmlsbD0iIzlEMkUyRSIpICsgCiAgbGFicyh4PSJBdmVyYWdlIFVzZXIgUmF0aW5nIiwgeT0iTnVtYmVyIG9mIFVzZXJzIiwgdGl0bGU9IkRpc3RyaWJ1dGlvbiBvZiBBdmVyYWdlIFVzZXIgUmF0aW5nIikgKyB0aGVtZV9taW5pbWFsKCkgKwogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KGZhbWlseT0iUm9ib3RvIiksCiAgICAgICAgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0xNCwgaGp1c3Q9MC41LCBtYXJnaW4gPSBtYXJnaW4odCA9IDUsIHIgPSAwLCBiID0gOCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTIsIGIgPSAwLCBsID0gMCkpLCAKICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpKQpnZ3NhdmUoIkF2ZXJhZ2UgcmF0aW5nIGhpc3RvZ3JhbS5wbmciLCB3aWR0aD03LCBoZWlnaHQ9NSkgIAoKYGBgCgpgYGB7cn0KIyBkaXN0cmlidXRpb24gb2YgbW92aWUgcmF0aW5nIGJ5IHVzZXIKIyBjb2xvciBvcHRpb25zOiAjQUEyQjJCICM5RDJFMkUKZ2dwbG90KHJhdGluZy5kdCwgYWVzKHg9UmF0aW5nKSkgKyAKICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aD0wLjMsIGNvbD0iZ3JheSIsIGZpbGw9IiM5RDJFMkUiKSArIAogIGxhYnMoeD0iVXNlciBSYXRpbmcgb2YgYSBNb3ZpZSIsIHk9Ik51bWJlciBvZiBVc2VycyIsIHRpdGxlPSJEaXN0cmlidXRpb24gb2YgVXNlciBSYXRpbmdzIikgKyAKICB0aGVtZV9taW5pbWFsKCkgKwogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KGZhbWlseT0iUm9ib3RvIiksCiAgICAgICAgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0xNCwgaGp1c3Q9MC41LCBtYXJnaW4gPSBtYXJnaW4odCA9IDUsIHIgPSAwLCBiID0gOCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTIsIGIgPSAwLCBsID0gMCkpLCAKICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpKQpnZ3NhdmUoIlJhdGluZyBIaXN0b2dyYW0ucG5nIiwgd2lkdGg9NywgaGVpZ2h0PTUpCmBgYAoKYGBge3J9CiMgbG9vayBhdCBudW1iZXIgb2YgbW92aWVzIHJhdGVkIGJ5IHVzZXIKCiMgZ2V0IHRhYmxlIHdpdGggbnVtYmVyIG9mIG1vdmllcyByYXRlZCBieSBlYWNoIHVzZXIKdXNlci5yYXRpbmdzIDwtIHJhdGluZy5kdFssIC4oIk51bVJhdGVkIj0uTiksIGJ5PUN1c3RvbWVySURdCgojIGF2ZXJhZ2UgbnVtYmVyIG9mIG1vdmllcyByYXRlZApjYXQoIkF2ZXJhZ2UgbnVtYmVyIG9mIG1vdmllcyByYXRlZDoiLG1lYW4odXNlci5yYXRpbmdzJE51bVJhdGVkKSkKCiMgbWVkaWFuIG51bWJlciBvZiBtb3ZpZXMgcmF0ZWQKY2F0KCJcbk1lZGlhbiBudW1iZXIgb2YgbW92aWVzIHJhdGVkOiIsbWVkaWFuKHVzZXIucmF0aW5ncyROdW1SYXRlZCkpCgojIG1heCBudW1iZXIgb2YgbW92dmllcyByYXRlZApjYXQoIlxuTWF4IG51bWJlciBvZiBtb3ZpZXMgcmF0ZWQ6IixtYXgodXNlci5yYXRpbmdzJE51bVJhdGVkKSkKYGBgCkEgbnVtYmVyIG9mIHVzZXJzIGhhZCB2ZXJ5IGhpZ2ggbW92aWUgcmF0aW5ncywgZS5nLiBpbiB0aGUgNDAwMHMgdGhhdCB3b3VsZCBpbXBseSAxMCsgbW92aWVzIHNlZW4gcGVyIGRheSBvbiBhdmVyYWdlLiBUaGlzIG1heSBiZSBkdWUgdG8gbXVsdGlwbGUgaW5kaXZpZHVhbHMgc2hhcmluZyBhbiBhY2NvdW50LCBvciBkdWUgdG8gdGhlIHVzZSBvZiBvbi1zaXRlIHN1cnZleXMgdG8gZ2V0IHJhdGluZ3Mgb2YgbW92aWVzIGEgdXNlciBzYXcgaW4gdGhlIHBhc3QuCgpgYGB7cn0KIyBnZXQgVG9wMTAgbW92aWVzIHdpdGggaGlnaGVzdCBudW1iZXIgb2YgcmF0aW5ncwptb3ZpZXMuaW5mbyA8LSByYXRpbmcuZHRbLCAuKCJOdW1iZXJvZlJhdGluZ3MiPS5OLCAiQXZnUmF0aW5nIj1tZWFuKFJhdGluZykpLCBieT1Nb3ZpZUlEXQptb3ZpZXMuaW5mbyA8LSBtZXJnZShtb3ZpZXMuaW5mbywgbW92aWVzLmR0LCBieT0iTW92aWVJRCIpCmBgYAoKYGBge3J9CiMgZ2V0IFRvcDEwIG1vdmllcyB3aXRoIGhpZ2hlc3QgbnVtYmVyIG9mIHJhdGluZ3MKcHJpbnQoIk1vdmllcyB3aXRoIGhpZ2hlc3QgbnVtYmVyIG9mIHJhdGluZ3MiKQpoZWFkKG1vdmllcy5pbmZvW29yZGVyKC1OdW1iZXJvZlJhdGluZ3MpXSRUaXRsZSwgMTApCmBgYAoKYGBge3J9CiMgZ2V0IFRvcDEwIG1vdmllcyB3aXRoIGhpZ2hlc3QgYXZlcmFnZSByYXRpbmcKcHJpbnQoIk1vdmllcyB3aXRoIGhpZ2hlc3QgYXZlcmFnZSByYXRpbmdzIikKaGVhZChtb3ZpZXMuaW5mb1tvcmRlcigtQXZnUmF0aW5nKV0kVGl0bGUsIDEwKQpgYGAKCmBgYHtyfQojIEZ1bGwgcGxvdApnZ3Bsb3QobW92aWVzLmluZm8sIGFlcyh4PU51bWJlcm9mUmF0aW5ncywgeT1BdmdSYXRpbmcpKSArIAogIGdlb21fc21vb3RoKG1ldGhvZD0ibG9lc3MiLCBzZT1GLCBjb2w9IiM5RDJFMkUiLCBzaXplPTEuMSkgKyAKICBsYWJzKHRpdGxlPSJBdmVyYWdlIFJhdGluZyB2ZXJzdXMgTW92aWUgRGVncmVlIENlbnRyYWxpdHkiLCB4PSJNb3ZpZSBEZWdyZWUgQ2VudHJhbGl0eSAoaW4gYmlwYXJ0aXRlIG5ldHdvcmspIiwgeT0iQXZlcmFnZSBSYXRpbmciKSArIAogIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX3hfY29udGludW91cyhicmVha3M9c2VxKDAsNjAwMDAsMTAwMDApKSArCiAgdGhlbWUodGV4dD1lbGVtZW50X3RleHQoZmFtaWx5PSJSb2JvdG8iKSwKICAgICAgICBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTE0LCBoanVzdD0wLjUsIG1hcmdpbiA9IG1hcmdpbih0ID0gNSwgciA9IDAsIGIgPSA4LCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSAxMiwgYiA9IDAsIGwgPSAwKSksIAogICAgICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDEwLCByID0gMCwgYiA9IDAsIGwgPSAwKSkpCmdnc2F2ZSgiQXZlcmFnZSByYXRpbmcgdnMgZGVncmVlLnBuZyIsIHdpZHRoPTcsIGhlaWdodD01KQpgYGAKCmBgYHtyfQojIFpvb21lZCBJbiBQbG90CmdncGxvdChtb3ZpZXMuaW5mb1tOdW1iZXJvZlJhdGluZ3M8MzAwMCxdLCBhZXMoeD1OdW1iZXJvZlJhdGluZ3MsIHk9QXZnUmF0aW5nKSkgKyAKICBnZW9tX3Ntb290aChtZXRob2Q9ImxvZXNzIiwgc2U9RiwgY29sPSIjOUQyRTJFIiwgc2l6ZT0xLjEpICsgCiAgbGFicyh0aXRsZT0iWm9vbWVkIGluOiBBdmVyYWdlIFJhdGluZyB2ZXJzdXMgTW92aWUgRGVncmVlIENlbnRyYWxpdHkiLCB4PSJNb3ZpZSBEZWdyZWUgQ2VudHJhbGl0eSAoaW4gYmlwYXJ0aXRlIG5ldHdvcmspIiwgeT0iQXZlcmFnZSBSYXRpbmciKSArIAogIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX3hfY29udGludW91cyhicmVha3M9c2VxKDAsMzAwMCw1MDApKSArCiAgdGhlbWUodGV4dD1lbGVtZW50X3RleHQoZmFtaWx5PSJSb2JvdG8iKSwKICAgICAgICBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTE0LCBoanVzdD0wLjUsIG1hcmdpbiA9IG1hcmdpbih0ID0gNSwgciA9IDAsIGIgPSA4LCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSAxMiwgYiA9IDAsIGwgPSAwKSksIAogICAgICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDEwLCByID0gMCwgYiA9IDAsIGwgPSAwKSkpCmdnc2F2ZSgiQXZlcmFnZSByYXRpbmcgdnMgZGVncmVlX1pPT01FRC5wbmciLCB3aWR0aD03LCBoZWlnaHQ9NSkKYGBgCgpgYGB7cn0KIyBMaW5lYXIgUmVncmVzc2lvbiBvbiBtb3ZpZXMgd2l0aCAxNTAwIG9yIGZld2VyIHJhdGluZ3MKcmVncmVzcy5kdCA8LSBtb3ZpZXMuaW5mb1tOdW1iZXJvZlJhdGluZ3MgPD0gMTAwMCxdICMgY3JlYXRlIHN1YnNldCBvZiBtb3ZpZXMgd2l0aCAxNTAwIG9yIGZld2VyIHJhdGluZ3MKc2V0bmFtZXMocmVncmVzcy5kdCwgIk51bWJlcm9mUmF0aW5ncyIsICJEZWdyZWUiKSAjIGNoYW5nZSBuYW1lIHRvIGRlZ3JlZQpzdW1tYXJ5KGxtKEF2Z1JhdGluZ35EZWdyZWUsIGRhdGE9cmVncmVzcy5kdCkpICMgcmVncmVzc2lvbiBvbiBzdWJzZXQKCnN1bW1hcnkobG0oQXZnUmF0aW5nfk51bWJlcm9mUmF0aW5ncywgZGF0YT1tb3ZpZXMuaW5mbykpICMgcmVncmVzc2lvbiBvbiBhbGwgZGF0YQpgYGAKCmBgYHtyfQojIGRpc3RyaWJ1dGlvbiBvZiBudW1iZXIgb2YgbW92aWVzIHJhdGVkIGJ5IHVzZXIsIGxpbWl0IGF4aXMgdG8gMTAwMCsgbW92aWVzCnBsb3QucmF0aW5ncyA8LSB1c2VyLnJhdGluZ3NbLC4oTnVtUmF0ZWQgPSBpZmVsc2UoTnVtUmF0ZWQ+PTEwMDAsIDEwMDAsIE51bVJhdGVkKSldCmdncGxvdChwbG90LnJhdGluZ3MsIGFlcyh4PU51bVJhdGVkKSkgKyAKICAgIGdlb21faGlzdG9ncmFtKGJpbndpZHRoPTgsIGNvbD0iZ3JheSIsIGZpbGw9IiM5RDJFMkUiKSArIAogICAgbGFicyh4PSJOdW1iZXIgb2YgTW92aWVzIFJhdGVkIiwgeT0iTnVtYmVyIG9mIFVzZXJzIiwgdGl0bGU9IkRpc3RyaWJ1dGlvbiBvZiBOdW1iZXIgb2YgTW92aWVzIFJhdGVkIikgKyAKICAgIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX3hfY29udGludW91cyhicmVha3M9c2VxKDEwMCwxMDAwLDEwMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbHM9YygiMTAwIiwiMjAwIiwiMzAwIiwiNDAwIiwiNTAwIiwiNjAwIiwiNzAwIiwiODAwIiwiOTAwIiwgIjEwMDArIikpICsKICAjc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcz1zZXEoMCw2MDAwMCwxMDAwMCkpICsKICAgIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KGZhbWlseT0iUm9ib3RvIiksCiAgICAgICAgICBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTE0LCBoanVzdD0wLjUsIG1hcmdpbiA9IG1hcmdpbih0ID0gNSwgciA9IDAsIGIgPSA4LCBsID0gMCkpLAogICAgICAgICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEyLCBiID0gMCwgbCA9IDApKSwgCiAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpKQpnZ3NhdmUoIm51bWJlciBvZiBtb3ZpZXMgcmF0ZWQgaGlzdG9ncmFtLnBuZyIsIHdpZHRoPTcsIGhlaWdodD01KQpgYGAKCiMjIyMgQnVpbGRpbmcgTmV0d29ya3M6IE1PVklFLVVTRVIgTkVUV09SSwoKYGBge3J9CiMgcHJlcCBmb3IgbWFraW5nIG5ldHdvcmsKcmF0aW5nLmR0WyxDdXN0b21lcklEIDo9IHN1YigiXiIsICJ1IiwgQ3VzdG9tZXJJRCApXQpyYXRpbmcuZHRbLCBNb3ZpZUlEIDo9IGFzLmNoYXJhY3RlcihNb3ZpZUlEKV0KCiMgbWFrZSBiaXBhcnRpdGUgZ3JhcGgKZ3JhcGguYnAgPC0gZ3JhcGguZGF0YS5mcmFtZShyYXRpbmcuZHRbLDE6Ml0sIGRpcmVjdGVkPUZBTFNFKSAjIG1ha2UgZ2VuZXJhbCB1bmRpcmVjdGVkIGdyYXBoClYoZ3JhcGguYnApJHR5cGUgPC0gVihncmFwaC5icCkkbmFtZSAlaW4lIHJhdGluZy5kdCRNb3ZpZUlEICMgc3BlY2lmeSB0eXBlIHRvIG1ha2UgYmlwYXJ0aXRlCkUoZ3JhcGguYnApJHdlaWdodCA8LSByYXRpbmcuZHQkUmF0aW5nICMgYWRkIGluIHJhdGluZyBhcyB3ZWlnaHQKCiMgbG9vayBhdCBncmFwaApncmFwaC5icApgYGAKCmBgYHtyIGZpZy53aWR0aD0yMCwgZmlnLmhlaWdodD0yMH0KIyBWaXN1YWxpemUgZ3JhcGggbGF5b3V0CmJwLnN1YnBsb3QgPC0gaW5kdWNlZF9zdWJncmFwaChncmFwaC5icCx2PXNhbXBsZSh1bmxpc3QoVihncmFwaC5icCkkbmFtZSksIDc1MDApKQoKIyBkZWZpbmUgY29sb3IgYW5kIHNoYXBlIG1hcHBpbmdzLgpjb2wgPC0gYygiZ3JheTg1IiwgIiM5RDJFMkUiKQpzaGFwZSA8LSBjKCJjaXJjbGUiLCAic3F1YXJlIikKCnBsb3QoYnAuc3VicGxvdCwKICB2ZXJ0ZXguY29sb3IgPSBjb2xbYXMubnVtZXJpYyhWKGJwLnN1YnBsb3QpJHR5cGUpKzFdLAogIHZlcnRleC5zaGFwZSA9IHNoYXBlW2FzLm51bWVyaWMoVihicC5zdWJwbG90KSR0eXBlKSsxXSwgbGF5b3V0PWxheW91dF9hc19iaXBhcnRpdGUoYnAuc3VicGxvdCwgaGdhcD0zMCksCiAgdmVydGV4LmZyYW1lLmNvbG9yPSJncmF5NjAiLAogIGVkZ2UuY29sb3IgPSAiI0U1QUFBQSIsCiAgdmVydGV4LmxhYmVsPSIiLCB2ZXJ0ZXguc2l6ZT01KQpgYGAKCiMjIyMgQnVpbGRpbmcgTmV0d29ya3M6IE1PVklFLU1PVklFIE5FVFdPUksKCmBgYHtyfQojIG1ha2UgYmlwYXJ0aXRlIGdyYXBoIG9uIG1vdmllLW1vdmllIG5ldHdvcmsKZ3JhcGguYnAyIDwtIGdyYXBoLmRhdGEuZnJhbWUocmF0aW5nLmR0WywyOjFdLCBkaXJlY3RlZD1GQUxTRSkgIyBtYWtlIGdlbmVyYWwgdW5kaXJlY3RlZCBncmFwaApWKGdyYXBoLmJwMikkdHlwZSA8LSBWKGdyYXBoLmJwMikkbmFtZSAlaW4lIHJhdGluZy5kdCRDdXN0b21lcklEICMgc3BlY2lmeSB0eXBlIHRvIG1ha2UgYmlwYXJ0aXRlCgojIGxvb2sgYXQgZ3JhcGgKZ3JhcGguYnAyCmBgYAoKYGBge3J9Cm1vdi5tdHggPC0gYXNfaW5jaWRlbmNlX21hdHJpeChncmFwaC5icDIpICMgZ2V0IGFmZmlsaWF0aW9uIG1hdHJpeCBmcm9tIGNoYXJ0Cm1vdi5zcC5tdHggPC0gYXMobW92Lm10eCwgInNwYXJzZU1hdHJpeCIpICMgZW5jb2RlIGFzIHNwYXJzZSBtYXRyaXgKbW92LmNvYWZmaWwubXR4IDwtIHRjcm9zc3Byb2QobW92LnNwLm10eCkgIyBnZXQgY28tYWZmaWxpYXRpb24gbWF0cml4IHRvIG1ha2UgbW92aWUgbmV0d29yawpgYGAKCmBgYHtyfQojIG1ha2UgbW92aWUtbW92aWUgY29hZmZpbGlhdGlvbiBuZXR3b3JrCmdyYXBoLm1vdmllcyA8LSBncmFwaF9mcm9tX2FkamFjZW5jeV9tYXRyaXgobW92LmNvYWZmaWwubXR4LCBtb2RlPSJ1bmRpcmVjdGVkIiwgZGlhZz1GQUxTRSwgd2VpZ2h0PVRSVUUpICMga2VlcCBkaWFnb25hbHMgYmVjYXVzZSBpbmRpY2F0ZSBvd24gcmF0aW5nIHN0cmVuZ3RoPwpgYGAKCmBgYHtyIGZpZy53aWR0aD0yMCwgZmlnLmhlaWdodD0yMH0KZ3JhcGgubW92aWVzCgptb3Yuc3VicGxvdCA8LSBpbmR1Y2VkX3N1YmdyYXBoKGdyYXBoLm1vdmllcyx2PXNhbXBsZSh1bmxpc3QoVihncmFwaC5tb3ZpZXMpJG5hbWUpLCAxMDAwKSkKcGxvdC5pZ3JhcGgobW92LnN1YnBsb3QsIHZlcnRleC5sYWJlbD0iIiwgdmVydGV4LmNvbG9yPSJncmF5NzAiLCB2ZXJ0ZXguZnJhbWUuY29sb3I9ImdyYXkzMCIsCiAgICAgICAgICAgIGVkZ2UuY29sb3I9IiNFNUFBQUEiLCB2ZXJ0ZXguc2l6ZT0zLCBsYXlvdXQ9bGF5b3V0X3dpdGhfa2spCgpgYGAKCmBgYHtyfQojIGNhbGN1bGF0ZSBjby1hZmZpbGlhdGlvbiBjZW50cmFsaXR5IG1lYXN1cmVzCmRlZ3JlZS5zY29yZTIgPC0gZGVncmVlKGdyYXBoLm1vdmllcykKY2xvc2VuZXNzLnNjb3JlMiA8LSBjbG9zZW5lc3MoZ3JhcGgubW92aWVzKQplaWdlbi5zY29yZTIgPC0gZWlnZW5fY2VudHJhbGl0eShncmFwaC5tb3ZpZXMpCgptb3ZpZXMucmF0aW5ncy4yIDwtIHJhdGluZy5kdFtNb3ZpZUlEICVpbiUgVihncmFwaC5tb3ZpZXMpJG5hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuKCJBdmdSYXRpbmciPW1lYW4oUmF0aW5nKSksIGJ5PU1vdmllSURdCm1vdmllcy5wZXJmb3JtYW5jZS4yIDwtIGRhdGEudGFibGUoIk1vdmllSUQiPVYoZ3JhcGgubW92aWVzKSRuYW1lLCAiRGVncmVlIj1kZWdyZWUuc2NvcmUyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJDbG9zZW5lc3MiPWNsb3NlbmVzcy5zY29yZTIpCm1vdmllcy5wZXJmb3JtYW5jZS4yIDwtIGRhdGEudGFibGUoIk1vdmllSUQiPVYoZ3JhcGgubW92aWVzKSRuYW1lLCAiRGVncmVlIj1kZWdyZWUuc2NvcmUyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiQ2xvc2VuZXNzIj1jbG9zZW5lc3Muc2NvcmUyLCAiRWlnZW5DZW50cmFsaXR5Ij1laWdlbi5zY29yZTIkdmVjdG9yKQptb3ZpZXMucGVyZm9ybWFuY2UuMiA8LSBtZXJnZShtb3ZpZXMucGVyZm9ybWFuY2UuMiwgbW92aWVzLnJhdGluZ3MuMiwgYnk9Ik1vdmllSUQiKQpgYGAKCmBgYHtyfQojIE1vdmllLU1vdmllIERlZ3JlZSBDZW50cmFsaXR5CmdncGxvdChtb3ZpZXMucGVyZm9ybWFuY2UuMiwgYWVzKHg9RGVncmVlLCB5PUF2Z1JhdGluZykpICsKICBnZW9tX3Ntb290aChtZXRob2Q9ImxvZXNzIiwgc2U9RiwgY29sPSIjOUQyRTJFIiwgc2l6ZT0xLjEpICsKICBsYWJzKHRpdGxlPSJBdmVyYWdlIFJhdGluZyB2ZXJzdXMgTW92aWUtTW92aWUgRGVncmVlIENlbnRyYWxpdHkiLCB4PSJNb3ZpZSBEZWdyZWUgQ2VudHJhbGl0eSAoaW4gY28tYWZmaWxpYXRpb24gbmV0d29yaykiLCB5PSJBdmVyYWdlIFJhdGluZyIpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIHRoZW1lKHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KHNpemU9MTQsIGhqdXN0PTAuNSwgbWFyZ2luID0gbWFyZ2luKHQgPSA1LCByID0gMCwgYiA9IDgsIGwgPSAwKSksCiAgICAgICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEyLCBiID0gMCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpKQpnZ3NhdmUoIm1vdmllLW1vdmllIGRlZ3JlZSB2cyBhdmcgcmF0aW5nLnBuZyIsIHdpZHRoPTcsIGhlaWdodD01KQpgYGAKCmBgYHtyfQojIE1vdmllLU1vdmllIENsb3NlbmVzcyBDZW50cmFsaXR5CmdncGxvdChtb3ZpZXMucGVyZm9ybWFuY2UuMiwgYWVzKHg9Q2xvc2VuZXNzLCB5PUF2Z1JhdGluZykpICsKICBnZW9tX3Ntb290aChtZXRob2Q9ImxvZXNzIiwgc2U9RiwgY29sPSIjOUQyRTJFIiwgc2l6ZT0xLjEpICsKICBsYWJzKHRpdGxlPSJBdmVyYWdlIFJhdGluZyB2ZXJzdXMgTW92aWUtTW92aWUgQ2xvc2VuZXNzIENlbnRyYWxpdHkiLCB4PSJNb3ZpZSBDbG9zZW5lc3MgQ2VudHJhbGl0eSAoaW4gY28tYWZmaWxpYXRpb24gbmV0d29yaykiLCB5PSJBdmVyYWdlIFJhdGluZyIpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KGZhbWlseT0iUm9ib3RvIiksCiAgICAgICAgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0xNCwgaGp1c3Q9MC41LCBtYXJnaW4gPSBtYXJnaW4odCA9IDUsIHIgPSAwLCBiID0gOCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTIsIGIgPSAwLCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDEwLCByID0gMCwgYiA9IDAsIGwgPSAwKSkpCmdnc2F2ZSgibW92aWUtbW92aWUgY2xvc2VuZXNzIHZzIGF2ZyByYXRpbmcucG5nIiwgd2lkdGg9NywgaGVpZ2h0PTUpCmBgYAoKYGBge3J9CiMgIyBNb3ZpZS1Nb3ZpZSBFaWdlbiBDZW50cmFsaXR5CmdncGxvdChtb3ZpZXMucGVyZm9ybWFuY2UuMiwgYWVzKHg9RWlnZW5DZW50cmFsaXR5LCB5PUF2Z1JhdGluZykpICsKICBnZW9tX3Ntb290aChtZXRob2Q9ImxvZXNzIiwgc2U9RiwgY29sPSIjOUQyRTJFIiwgc2l6ZT0xLjEpICsKICBsYWJzKHRpdGxlPSJBdmVyYWdlIFJhdGluZyB2ZXJzdXMgTW92aWUtTW92aWUgRWlnZW4gQ2VudHJhbGl0eSIsIHg9Ik1vdmllIEVpZ2VuIENlbnRyYWxpdHkgKGluIGNvLWFmZmlsaWF0aW9uIG5ldHdvcmspIiwgeT0iQXZlcmFnZSBSYXRpbmciKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZSh0ZXh0PWVsZW1lbnRfdGV4dChmYW1pbHk9IlJvYm90byIpLAogICAgICAgIHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KHNpemU9MTQsIGhqdXN0PTAuNSwgbWFyZ2luID0gbWFyZ2luKHQgPSA1LCByID0gMCwgYiA9IDgsIGwgPSAwKSksCiAgICAgICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEyLCBiID0gMCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpKQpnZ3NhdmUoIm1vdmllLW1vdmllIGVpZ2VuIHZzIGF2ZyByYXRpbmcucG5nIiwgd2lkdGg9NywgaGVpZ2h0PTUpCmBgYAoKYGBge3J9CiMgaG93IHRvIGludGVwcmV0CnN1bW1hcnkobG0oQXZnUmF0aW5nIH4gRGVncmVlLCBkYXRhID0gbW92aWVzLnBlcmZvcm1hbmNlLjIpKSAjIGRlZ3JlZQpzdW1tYXJ5KGxtKEF2Z1JhdGluZyB+IENsb3NlbmVzcywgZGF0YSA9IG1vdmllcy5wZXJmb3JtYW5jZS4yKSkgIyBjbG9zZW5lc3MKc3VtbWFyeShsbShBdmdSYXRpbmcgfiBFaWdlbkNlbnRyYWxpdHksIGRhdGEgPSBtb3ZpZXMucGVyZm9ybWFuY2UuMikpICMgRWlnZW5DZW50cmFsaXR5CmBgYAoKIyMjIyBCdWlsZGluZyBOZXR3b3JrczogVVNFUi1VU0VSIE5FVFdPUksKCmBgYHtyfQojIGxpbWl0IHVzZXItdXNlciBuZXR3b3JrIHRvIHVzZXJzIHdpdGggYXQgbGVhc3QgMjAwIHJhdGluZ3MKdXNlcnMuc2FtcGxlIDwtIGNvcHkocmF0aW5nLmR0KQp1c2Vycy5zYW1wbGUgPC0gdXNlcnMuc2FtcGxlWywgTnVtUmF0aW5ncyA6PSAuTiwgYnk9Q3VzdG9tZXJJRF0KdXNlcnMuc2FtcGxlIDwtIHVzZXJzLnNhbXBsZVtOdW1SYXRpbmdzID49IDIwMCwgXQoKIyBjcmVhdGUgYmlwYXJ0aXRlIGdyYXBoIHdpdGggbGVzcyBkYXRhCmdyYXBoLmJwMyA8LSBncmFwaC5kYXRhLmZyYW1lKHVzZXJzLnNhbXBsZVssMToyXSwgZGlyZWN0ZWQ9RkFMU0UpICMgbWFrZSBnZW5lcmFsIHVuZGlyZWN0ZWQgZ3JhcGgKVihncmFwaC5icDMpJHR5cGUgPC0gVihncmFwaC5icDMpJG5hbWUgJWluJSB1c2Vycy5zYW1wbGUkTW92aWVJRCAjIHNwZWNpZnkgdHlwZSB0byBtYWtlIGJpcGFydGl0ZQpFKGdyYXBoLmJwMykkd2VpZ2h0IDwtIHVzZXJzLnNhbXBsZSRSYXRpbmcgIyBhZGQgaW4gcmF0aW5nIGFzIHdlaWdodAoKIyBnZXQgY29hZmZpbGlhdGlvbiBtYXRyaXggZnJvbSBiaXBhcnRpdGUgZ3JhcGgKdXNlcnMubXR4IDwtIGFzX2luY2lkZW5jZV9tYXRyaXgoZ3JhcGguYnAzKSAjIGdldCBhZmZpbGlhdGlvbiBtYXRyaXggZnJvbSBjaGFydAp1c2Vycy5zcC5tdHggPC0gYXModXNlcnMubXR4LCAic3BhcnNlTWF0cml4IikgIyBlbmNvZGUgYXMgc3BhcnNlIG1hdHJpeAp1c2Vycy5jb2FmZmlsLm10eCA8LSB0Y3Jvc3Nwcm9kKHVzZXJzLnNwLm10eCkgIyBnZXQgY28tYWZmaWxpYXRpb24gbWF0cml4IHRvIG1ha2UgbW92aWUgbmV0d29yawpgYGAKCmBgYHtyfQojIG1ha2UgdXNlci11c2VyIGNvYWZmaWxpYXRpb24gbmV0d29yawpncmFwaC51c2VycyA8LSBncmFwaF9mcm9tX2FkamFjZW5jeV9tYXRyaXgodXNlcnMuY29hZmZpbC5tdHgsIG1vZGU9InVuZGlyZWN0ZWQiLCBkaWFnPUZBTFNFLCB3ZWlnaHQ9VFJVRSkgIyBrZWVwIGRpYWdvbmFscyBiZWNhdXNlIGluZGljYXRlIG93biByYXRpbmcgc3RyZW5ndGg/CmBgYAoKYGBge3IgZmlnLndpZHRoPTIwLCBmaWcuaGVpZ2h0PTIwfQojZ3JhcGgudXNlcnMKdXNlcnMuc3VicGxvdCA8LSBpbmR1Y2VkX3N1YmdyYXBoKGdyYXBoLnVzZXJzLHY9c2FtcGxlKHVubGlzdChWKGdyYXBoLnVzZXJzKSRuYW1lKSwgMzAwKSkKcGxvdC5pZ3JhcGgodXNlcnMuc3VicGxvdCwgdmVydGV4LmNvbG9yPSJncmF5NzAiLCB2ZXJ0ZXgubGFiZWw9IiIsIHZlcnRleC5mcmFtZS5jb2xvcj0iZ3JheTMwIiwKICAgICAgICAgICAgZWRnZS5jb2xvcj0iI0U1QUFBQSIsIHZlcnRleC5zaXplPTMsIGxheW91dD1sYXlvdXRfd2l0aF9raykKYGBgCgpgYGB7cn0KIyBjYWxjdWxhdGUgY28tYWZmaWxpYXRpb24gY2VudHJhbGl0eSBtZWFzdXJlcwpkZWdyZWUuc2NvcmUzIDwtIGRlZ3JlZShncmFwaC51c2VycykKY2xvc2VuZXNzLnNjb3JlMyA8LSBjbG9zZW5lc3MoZ3JhcGgudXNlcnMpCmVpZ2VuLnNjb3JlMyA8LSBlaWdlbl9jZW50cmFsaXR5KGdyYXBoLnVzZXJzKQoKbW92aWVzLnJhdGluZ3MuMyA8LSByYXRpbmcuZHRbQ3VzdG9tZXJJRCAlaW4lIFYoZ3JhcGgudXNlcnMpJG5hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuKCJBdmdSYXRpbmciPW1lYW4oUmF0aW5nKSksIGJ5PUN1c3RvbWVySURdCm1vdmllcy5wZXJmb3JtYW5jZS4zIDwtIGRhdGEudGFibGUoIkN1c3RvbWVySUQiPVYoZ3JhcGgudXNlcnMpJG5hbWUsICJEZWdyZWUiPWRlZ3JlZS5zY29yZTMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIkNsb3NlbmVzcyI9Y2xvc2VuZXNzLnNjb3JlMykKbW92aWVzLnBlcmZvcm1hbmNlLjMgPC0gZGF0YS50YWJsZSgiQ3VzdG9tZXJJRCI9VihncmFwaC51c2VycykkbmFtZSwgIkRlZ3JlZSI9ZGVncmVlLnNjb3JlMywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiQ2xvc2VuZXNzIj1jbG9zZW5lc3Muc2NvcmUzLCAiRWlnZW5DZW50cmFsaXR5Ij1laWdlbi5zY29yZTMkdmVjdG9yKQptb3ZpZXMucGVyZm9ybWFuY2UuMyA8LSBtZXJnZShtb3ZpZXMucGVyZm9ybWFuY2UuMywgbW92aWVzLnJhdGluZ3MuMywgYnk9IkN1c3RvbWVySUQiKQpgYGAKCmBgYHtyfQojIFVzZXItVXNlciBEZWdyZWUgQ2VudHJhbGl0eQpnZ3Bsb3QobW92aWVzLnBlcmZvcm1hbmNlLjMsIGFlcyh4PURlZ3JlZSwgeT1BdmdSYXRpbmcpKSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kPSJsb2VzcyIsIHNlPUYsIGNvbD0iIzlEMkUyRSIsIHNpemU9MS4xKSArCiAgbGFicyh0aXRsZT0iQXZlcmFnZSBSYXRpbmcgdmVyc3VzIFVzZXItVXNlciBEZWdyZWUgQ2VudHJhbGl0eSIsIHg9IlVzZXIgRGVncmVlIENlbnRyYWxpdHkgKGluIGNvLWFmZmlsaWF0aW9uIG5ldHdvcmspIiwgeT0iQXZlcmFnZSBSYXRpbmciKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZShwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTE0LCBoanVzdD0wLjUsIG1hcmdpbiA9IG1hcmdpbih0ID0gNSwgciA9IDAsIGIgPSA4LCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSAxMiwgYiA9IDAsIGwgPSAwKSksCiAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMTAsIHIgPSAwLCBiID0gMCwgbCA9IDApKSkKZ2dzYXZlKCJ1c2VyLXVzZXIgZGVncmVlIHZzIGF2ZyByYXRpbmcucG5nIiwgd2lkdGg9NywgaGVpZ2h0PTUpCmBgYAoKYGBge3J9CiMgVXNlci1Vc2VyIENsb3NlbmVzcyBDZW50cmFsaXR5CmdncGxvdChtb3ZpZXMucGVyZm9ybWFuY2UuMywgYWVzKHg9Q2xvc2VuZXNzLCB5PUF2Z1JhdGluZykpICsKICBnZW9tX3Ntb290aChtZXRob2Q9ImxvZXNzIiwgc2U9RiwgY29sPSIjOUQyRTJFIiwgc2l6ZT0xLjEpICsKICBsYWJzKHRpdGxlPSJBdmVyYWdlIFJhdGluZyB2ZXJzdXMgVXNlci1Vc2VyIENsb3NlbmVzcyBDZW50cmFsaXR5IiwgeD0iVXNlciBDbG9zZW5lc3MgQ2VudHJhbGl0eSAoaW4gY28tYWZmaWxpYXRpb24gbmV0d29yaykiLCB5PSJBdmVyYWdlIFJhdGluZyIpICsKICB0aGVtZV9taW5pbWFsKCkgKwogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KGZhbWlseT0iUm9ib3RvIiksCiAgICAgICAgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0xNCwgaGp1c3Q9MC41LCBtYXJnaW4gPSBtYXJnaW4odCA9IDUsIHIgPSAwLCBiID0gOCwgbCA9IDApKSwKICAgICAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTIsIGIgPSAwLCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDEwLCByID0gMCwgYiA9IDAsIGwgPSAwKSkpCmdnc2F2ZSgidXNlci11c2VyIGNsb3NlbmVzcyB2cyBhdmcgcmF0aW5nLnBuZyIsIHdpZHRoPTcsIGhlaWdodD01KQpgYGAKCmBgYHtyfQojICMgTW92aWUtTW92aWUgRWlnZW4gQ2VudHJhbGl0eQpnZ3Bsb3QobW92aWVzLnBlcmZvcm1hbmNlLjMsIGFlcyh4PUVpZ2VuQ2VudHJhbGl0eSwgeT1BdmdSYXRpbmcpKSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kPSJsb2VzcyIsIHNlPUYsIGNvbD0iIzlEMkUyRSIsIHNpemU9MS4xKSArCiAgbGFicyh0aXRsZT0iQXZlcmFnZSBSYXRpbmcgdmVyc3VzIFVzZXItVXNlciBFaWdlbiBDZW50cmFsaXR5IiwgeD0iVXNlciBFaWdlbiBDZW50cmFsaXR5IChpbiBjby1hZmZpbGlhdGlvbiBuZXR3b3JrKSIsIHk9IkF2ZXJhZ2UgUmF0aW5nIikgKwogIHRoZW1lX21pbmltYWwoKSArCiAgdGhlbWUodGV4dD1lbGVtZW50X3RleHQoZmFtaWx5PSJSb2JvdG8iKSwKICAgICAgICBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChzaXplPTE0LCBoanVzdD0wLjUsIG1hcmdpbiA9IG1hcmdpbih0ID0gNSwgciA9IDAsIGIgPSA4LCBsID0gMCkpLAogICAgICAgIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfdGV4dChtYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSAxMiwgYiA9IDAsIGwgPSAwKSksCiAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMTAsIHIgPSAwLCBiID0gMCwgbCA9IDApKSkKZ2dzYXZlKCJ1c2VyLXVzZXIgZWlnZW4gdnMgYXZnIHJhdGluZy5wbmciLCB3aWR0aD03LCBoZWlnaHQ9NSkKYGBgCgpgYGB7cn0KIyBob3cgdG8gaW50ZXByZXQKc3VtbWFyeShsbShBdmdSYXRpbmcgfiBEZWdyZWUsIGRhdGEgPSBtb3ZpZXMucGVyZm9ybWFuY2UuMykpICMgZGVncmVlCnN1bW1hcnkobG0oQXZnUmF0aW5nIH4gQ2xvc2VuZXNzLCBkYXRhID0gbW92aWVzLnBlcmZvcm1hbmNlLjMpKSAjIGNsb3NlbmVzcwpzdW1tYXJ5KGxtKEF2Z1JhdGluZyB+IEVpZ2VuQ2VudHJhbGl0eSwgZGF0YSA9IG1vdmllcy5wZXJmb3JtYW5jZS4zKSkgIyBFaWdlbkNlbnRyYWxpdHkKYGBgCgojIyMjIFJFQ09NTUVOREVSIExBQgoKYGBge3J9CiMgZ2V0IGl0ZW0tbW92aWUgbWF0cml4IGZyb20gZGlyZWN0ZWQgZ3JhcGgKaW5wdXQubXR4IDwtIGFzX2luY2lkZW5jZV9tYXRyaXgoZ3JhcGguYnAsIGF0dHI9IndlaWdodCIsIHNwYXJzZT1UUlVFKQoKIyBzdG9yZSBhcyByZWNvbW1lbmRlciBsYWIgbWF0cml4ICNhbmQgbm9ybWFsaXplCmlucHV0Lm10eCA8LSBhcyhpbnB1dC5tdHgsICJyZWFsUmF0aW5nTWF0cml4IikKYGBgCgpgYGB7cn0KIyBpbnNwZWN0IHRoZSByYXRpbmcgZGlzdHJpYnV0aW9ucwpoaXN0KGdldFJhdGluZ3MoaW5wdXQubXR4KSkKaGlzdChnZXRSYXRpbmdzKG5vcm1hbGl6ZShpbnB1dC5tdHgpKSwgYnJlYWtzPTEwMCkKaGlzdChnZXRSYXRpbmdzKG5vcm1hbGl6ZShpbnB1dC5tdHgsIG1ldGhvZD0iWi1zY29yZSIpKSwgYnJlYWtzPTEwMCkKaGlzdChjb2xNZWFucyhpbnB1dC5tdHgpLCBicmVha3M9MjApCmBgYAoKIyBUZXN0IGV4YW1wbGUgdG8gcHJvZHVjZSBzYW1wbGUgcmVjb21tZW5kYXRpb25zICh1cGRhdGVkIHRvIGdpdmUgcmFuZG9tIHNhbXBsZSkKYGBge3J9CiMgY3JlYXRlIGEgcmVjb21tZW5kZXIgb24gVUJDRgpyZWMubW9kZWwgPSBSZWNvbW1lbmRlcihpbnB1dC5tdHgsIG1ldGhvZCA9ICJVQkNGIikKCiMgZ2V0IGEgcmFuZG9tIHVzZXIgbmFtZQp0ZXN0LnVzZXIgPC0gc2FtcGxlKHJhdGluZy5kdCRDdXN0b21lcklELCAxKQoKIyBnZXQgdGhlIHRvcCAxMCBtb3ZpZXMgcmVjb21tZW5kZWQgZm9yIHVzZXIgWFgKcm93Q291bnRzKGlucHV0Lm10eFt0ZXN0LnVzZXJdKQoKIyB3aGF0IGhlIHJhdGVkIGhpZ2gKcmF0ZWQuaGlnaCA8LSByYXRpbmcuZHRbQ3VzdG9tZXJJRD09dGVzdC51c2VyICYgUmF0aW5nID4gMyxdCgpyZWMudGVzdC51c2VyID0gcHJlZGljdChyZWMubW9kZWwsIGlucHV0Lm10eFt0ZXN0LnVzZXIsXSwgbj0xMCkKcmVjLmNvbXBhcmUgPC0gYXMubnVtZXJpYyh1bmxpc3QoYXMocmVjLnRlc3QudXNlciwgImxpc3QiKSkpCgojIG1vdmllcyBoZSByYXRlZCBoaWdoCmhpZ2ggPC0gbW92aWVzLmR0W01vdmllSUQgJWluJSByYXRlZC5oaWdoJE1vdmllSUQsIC4oTW92aWVJRCxUaXRsZSldCgojIG1vdmllcyByZWNvbW1lbmRlZApyZWNvbW1lbmQgPC0gbW92aWVzLmR0W01vdmllSUQgJWluJSByZWMuY29tcGFyZSwgLihNb3ZpZUlELFRpdGxlKV0KCmhpZ2gKcmVjb21tZW5kCmBgYAoKIyBFdmFsdWF0aW9uCmBgYHtyfQojIGV2YWx1YXRlIGRpZmZlcmVudCBtZXRob2RzCmV2YWwgPSBldmFsdWF0aW9uU2NoZW1lKGlucHV0Lm10eCwgbWV0aG9kPSJzcGxpdCIsIHRyYWluPTAuNzUsIGdpdmVuID0gMjAsIGdvb2RSYXRpbmcgPSA0KQpldmFsCgojIGFsZ29yaXRobXMgKHBlcmZvcm0gbm9ybWFsaXphdGlvbiBhdXRvbWF0aWNhbGx5KQphbGdvcml0aG1zIDwtIGxpc3QoCiAgInJhbmRvbSBpdGVtcyIgPSBsaXN0KG5hbWU9IlJBTkRPTSIsIHBhcmFtPU5VTEwpLAogICJwb3B1bGFyIGl0ZW1zIiA9IGxpc3QobmFtZT0iUE9QVUxBUiIsIHBhcmFtPU5VTEwpLAogICJ1c2VyLWJhc2VkIENGIiA9IGxpc3QobmFtZT0iVUJDRiIsIHBhcmFtPWxpc3Qobm49NTApKSwKICAiaXRlbS1iYXNlZCBDRiIgPSBsaXN0KG5hbWU9IklCQ0YiLCBwYXJhbT1saXN0KGs9NTApKSwKICAiU1ZEIGFwcHJveGltYXRpb24iID0gbGlzdChuYW1lPSJTVkQiLCBwYXJhbT1saXN0KGsgPSA1MCkpKQpgYGAKCmBgYHtyfQojIGV2YWx1YXRlIHRvcC1OIHJlY29tbWVuZGF0aW9ucwpyZXN1bHRzMSA8LSBldmFsdWF0ZShldmFsLCBhbGdvcml0aG1zLCB0eXBlID0gInRvcE5MaXN0Iiwgbj1jKDEsIDUsIDEwLCAyMCwgNTApKQpgYGAKYGBge3J9CiMgUk9DIEN1cnZlCnBsb3QobWFpbj0iQ29tcGFyaXNvbiBvZiBST0MgY3VydmVzIGZvciA1IHJlY29tbWVuZGVyIG1ldGhvZHMiLCByZXN1bHRzMSwgbGVnZW5kPSJ0b3BsZWZ0IiwgY29sPWMoIiMyMzFGMjAiLCAiIzAwNjgwQSIsICIjMTQwMTUyIiwgIiMyMjc0QTUiLCAiIzlEMkUyRSIpLCBjZXg9MC44LCBsd2Q9MS4yLCBhbm5vdGF0ZT1jKDUpKQpgYGAKCmBgYHtyfQojIHByZWNpc2lvbi1yZWNhbGwgY3VydmUKcGxvdCh5PSJwcmVjL3JlYyIsIG1haW49IkNvbXBhcmlzb24gb2YgUk9DIGN1cnZlcyBmb3IgNSByZWNvbW1lbmRlciBtZXRob2RzIiwgcmVzdWx0czEsIGxlZ2VuZD0iYm90dG9tcmlnaHQiLCBjb2w9YygiIzIzMUYyMCIsICIjMDA2ODBBIiwgIiMxNDAxNTIiLCAiIzIyNzRBNSIsICIjOUQyRTJFIiksIGNleD0wLjgsIGx3ZD0xLjIsIGFubm90YXRlPWMoNSkpCmBgYAoKCmBgYHtyfQojIGV2YWx1YXRlIHJhdGluZ3MgcHJlZGljdGlvbgpyZXN1bHRzMiA8LSBldmFsdWF0ZShldmFsLCBhbGdvcml0aG1zLCB0eXBlID0gInJhdGluZ3MiKQoKIyBNU0UgLyBNQUUgcGxvdCBmb3IgcmF0aW5nCnBsb3QocmVzdWx0czIsIHlsaW0gPSBjKDAsMyksIG1haW49IkNvbXBhcmlzb24gb2YgUk1TRSwgTVNFLCBhbmQgTUFFIGZvciA1IHJlY29tbWVuZGVyIG1ldGhvZHMiKQpgYGAKYGBge3J9CiMgTVNFIC8gTUFFIHBsb3QgZm9yIHJhdGluZwpwbG90KHJlc3VsdHMyLCB5bGltID0gYygwLDMpLCBtYWluPSJDb21wYXJpc29uIG9mIFJNU0UsIE1TRSwgYW5kIE1BRSBmb3IgNSByZWNvbW1lbmRlciBtZXRob2RzIiwKY29sPWMoIiNBRkFGQUYiLCAiIzY3OTM2QiIsICIjMzE0RTg5IiwgIiM3QUE0QkMiLCAiIzlFNDU0NSIpKQpgYGAKCgoK